diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py
index 6371085..9d2cff2 100644
--- a/web/pgAdmin4.py
+++ b/web/pgAdmin4.py
@@ -7,7 +7,7 @@
 #
 ##########################################################################
 
-"""This is the main application entry point for pgAdmin 4. If running on 
+"""This is the main application entry point for pgAdmin 4. If running on
 a webserver, this will provide the WSGI interface, otherwise, we're going
 to start a web server."""
 
@@ -50,8 +50,11 @@ if not os.path.isfile(config.SQLITE_PATH):
 # Create the app!
 app = create_app()
 
+#if config.DEBUG:
+#    app.debug = True
+
 # Start the web server. The port number should have already been set by the
-# runtime if we're running in desktop mode, otherwise we'll just use the 
+# runtime if we're running in desktop mode, otherwise we'll just use the
 # Flask default.
 if 'PGADMIN_PORT' in globals():
     app.logger.debug('PGADMIN_PORT set in the runtime environment to %s', PGADMIN_PORT)
@@ -64,4 +67,3 @@ try:
     app.run(port=server_port)
 except IOError:
     app.logger.error("Error starting the app server: %s", sys.exc_info())
-
diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py
index ee8bc12..efd4b7c 100644
--- a/web/pgadmin/__init__.py
+++ b/web/pgadmin/__init__.py
@@ -9,28 +9,77 @@
 
 """The main pgAdmin module. This handles the application initialisation tasks,
 such as setup of logging, dynamic loading of modules etc."""
-
-from flask import Flask, abort, request
+from collections import defaultdict
+from flask import Flask, abort, request, current_app
 from flask.ext.babel import Babel
-from flask.ext.sqlalchemy import SQLAlchemy
-from flask.ext.security import Security, SQLAlchemyUserDatastore, login_required
+from flask.ext.security import Security, SQLAlchemyUserDatastore
 from flask_security.utils import login_user
 from flask_mail import Mail
 from htmlmin.minify import html_minify
 from settings.settings_model import db, Role, User
-
-import inspect, imp, logging, os, sys
+from importlib import import_module
+from werkzeug.local import LocalProxy
+from pgadmin.utils import PgAdminModule
+from werkzeug.utils import find_modules
+import sys
+import logging
 
 # Configuration settings
 import config
 
-# Global module list
-modules = [ ]
+
+class PgAdmin(Flask):
+
+    def find_submodules(self, basemodule):
+        for module_name in find_modules(basemodule, True):
+            if module_name in self.config['MODULE_BLACKLIST']:
+                self.logger.info('Skipping blacklisted module: %s' %
+                                module_name)
+                continue
+            self.logger.info('Examining potential module: %s' % module_name)
+            module = import_module(module_name)
+            for key, value in module.__dict__.items():
+                if isinstance(value, PgAdminModule):
+                    yield value
+
+    @property
+    def submodules(self):
+        for blueprint in self.blueprints.values():
+            if isinstance(blueprint, PgAdminModule):
+                yield blueprint
+
+    @property
+    def stylesheets(self):
+        stylesheets = []
+        for module in self.submodules:
+            stylesheets.extend(getattr(module, "stylesheets", []))
+        return stylesheets
+
+    @property
+    def javascripts(self):
+        stylesheets = []
+        for module in self.submodules:
+            stylesheets.extend(getattr(module, "javascripts", []))
+        return stylesheets
+
+    @property
+    def panels(self):
+        panels = []
+        for module in self.submodules:
+            panels.extend(module.get_panels())
+        return panels
+
+def _find_blueprint():
+    if request.blueprint:
+        return current_app.blueprints[request.blueprint]
+
+current_blueprint = LocalProxy(_find_blueprint)
+
 
 def create_app(app_name=config.APP_NAME):
     """Create the Flask application, startup logging and dynamically load
     additional modules (blueprints) that are found in this directory."""
-    app = Flask(__name__, static_url_path='/static')
+    app = PgAdmin(__name__, static_url_path='/static')
     app.config.from_object(config)
 
     ##########################################################################
@@ -42,7 +91,7 @@ def create_app(app_name=config.APP_NAME):
     app.logger.setLevel(logging.DEBUG)
     app.logger.handlers = []
 
-    # We also need to update the handler on the webserver in order to see request. 
+    # We also need to update the handler on the webserver in order to see request.
     # Setting the level prevents werkzeug from setting up it's own stream handler
     # thus ensuring all the logging goes through the pgAdmin logger.
     logger = logging.getLogger('werkzeug')
@@ -67,82 +116,48 @@ def create_app(app_name=config.APP_NAME):
     app.logger.info('Starting %s v%s...', config.APP_NAME, config.APP_VERSION)
     app.logger.info('################################################################################')
     app.logger.debug("Python syspath: %s", sys.path)
-    
+
     ##########################################################################
     # Setup i18n
     ##########################################################################
-    
+
     # Initialise i18n
     babel = Babel(app)
-    
+
     app.logger.debug('Available translations: %s' % babel.list_translations())
 
     @babel.localeselector
     def get_locale():
         """Get the best language for the user."""
         language = request.accept_languages.best_match(config.LANGUAGES.keys())
-        return language 
+        return language
 
     ##########################################################################
     # Setup authentication
     ##########################################################################
-   
+
     app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + config.SQLITE_PATH.replace('\\', '/')
 
     # Only enable password related functionality in server mode.
-    if config.SERVER_MODE == True:
+    if config.SERVER_MODE is True:
         # TODO: Figure out how to disable /logout and /login
         app.config['SECURITY_RECOVERABLE'] = True
         app.config['SECURITY_CHANGEABLE'] = True
 
     # Create database connection object and mailer
     db.init_app(app)
-    mail = Mail(app)
+    Mail(app)
 
     # Setup Flask-Security
     user_datastore = SQLAlchemyUserDatastore(db, User, Role)
-    security = Security(app, user_datastore)
+    Security(app, user_datastore)
 
     ##########################################################################
     # Load plugin modules
     ##########################################################################
-
-    path = os.path.dirname(os.path.realpath(__file__))
-    files = os.listdir(path)
-
-    for f in files:
-        d = os.path.join(path, f)
-        if os.path.isdir(d) and os.path.isfile(os.path.join(d, '__init__.py')):
-
-            if f in config.MODULE_BLACKLIST:
-                app.logger.info('Skipping blacklisted module: %s' % f)
-                continue
-
-            # Construct the "real" module name
-            f = 'pgadmin.' + f
-            
-            # Looks like a module, so import it, and register the blueprint if present
-            # We rely on the ordering of syspath to ensure we actually get the right
-            # module here. Note that we also try to load the 'hooks' module for
-            # the browser integration hooks and other similar functions.
-            app.logger.info('Examining potential module: %s' % d)
-            module = __import__(f, globals(), locals(), ['hooks', 'views'], -1)
-
-            # Add the module to the global module list
-            modules.append(module)
-            
-            # Register the blueprint if present
-            if 'views' in dir(module) and 'blueprint' in dir(module.views):
-                app.logger.info('Registering blueprint module: %s' % f)
-                app.register_blueprint(module.views.blueprint)
-                app.logger.debug('   - root_path:       %s' % module.views.blueprint.root_path)
-                app.logger.debug('   - static_folder:   %s' % module.views.blueprint.static_folder)
-                app.logger.debug('   - template_folder: %s' % module.views.blueprint.template_folder)
-                
-            # Register any sub-modules
-            if 'hooks' in dir(module) and 'register_submodules' in dir(module.hooks):
-                app.logger.info('Registering sub-modules in %s' % f)
-                module.hooks.register_submodules(app)
+    for module in app.find_submodules('pgadmin'):
+        app.logger.info('Registering blueprint module: %s' % module)
+        app.register_blueprint(module)
 
     ##########################################################################
     # Handle the desktop login
@@ -151,7 +166,7 @@ def create_app(app_name=config.APP_NAME):
     @app.before_request
     def before_request():
         """Login the default user if running in desktop mode"""
-        if config.SERVER_MODE == False:
+        if config.SERVER_MODE is False:
             user = user_datastore.get_user(config.DESKTOP_USER)
 
             # Throw an error if we failed to find the desktop user, to give
@@ -165,7 +180,7 @@ def create_app(app_name=config.APP_NAME):
 
     ##########################################################################
     # Minify output
-    ##########################################################################    
+    ##########################################################################
     @app.after_request
     def response_minify(response):
         """Minify html response to decrease traffic"""
@@ -177,10 +192,20 @@ def create_app(app_name=config.APP_NAME):
 
         return response
 
+    @app.context_processor
+    def inject_blueprint():
+        """Inject a reference to the current blueprint, if any."""
+        menu_items = defaultdict(list)
+        for blueprint in app.submodules:
+            menu_items.update(getattr(blueprint, "menu_items", {}))
+        return {
+            'current_app': current_app,
+            'current_blueprint': current_blueprint,
+            'menu_items': menu_items }
+
     ##########################################################################
     # All done!
     ##########################################################################
 
     app.logger.debug('URL map: %s' % app.url_map)
     return app
-
diff --git a/web/pgadmin/about/__init__.py b/web/pgadmin/about/__init__.py
index e69de29..5830c7c 100644
--- a/web/pgadmin/about/__init__.py
+++ b/web/pgadmin/about/__init__.py
@@ -0,0 +1,67 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2015, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""A blueprint module implementing the about box."""
+MODULE_NAME = 'about'
+
+from pgadmin.utils import PgAdminModule
+from pgadmin.utils.menu import MenuItem
+from flask import Response, current_app, render_template, __version__, url_for
+from flask.ext.babel import gettext
+from flask.ext.security import current_user, login_required
+
+import sys
+
+import config
+
+class AboutModule(PgAdminModule):
+
+    def get_own_menuitems(self):
+        return {
+            'help_items': [
+                MenuItem(name='mnu_about',
+                         priority=999,
+                         url='#',
+                         onclick='about_show()',
+                         label=gettext('About %(appname)s', appname=config.APP_NAME))
+            ]
+        }
+
+    def get_own_javascripts(self):
+        return [url_for('about.script')]
+
+
+blueprint = AboutModule(MODULE_NAME, __name__,
+                        static_url_path='')
+
+##########################################################################
+# A test page
+##########################################################################
+@blueprint.route("/")
+@login_required
+def index():
+    """Render the about box."""
+    info = { }
+    info['python_version'] = sys.version
+    info['flask_version'] = __version__
+    if config.SERVER_MODE == True:
+        info['app_mode'] = gettext('Server')
+    else:
+        info['app_mode'] = gettext('Desktop')
+    info['current_user'] = current_user.email
+
+    return render_template(MODULE_NAME + '/index.html', info=info)
+
+@blueprint.route("/about.js")
+@login_required
+def script():
+    """Render the required Javascript"""
+    return Response(response=render_template("about/about.js"),
+                    status=200,
+                    mimetype="application/javascript")
diff --git a/web/pgadmin/about/hooks.py b/web/pgadmin/about/hooks.py
deleted file mode 100644
index f0c0ac8..0000000
--- a/web/pgadmin/about/hooks.py
+++ /dev/null
@@ -1,28 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""Browser integration functions for the About module."""
-
-from flask import render_template, url_for
-from flask.ext.babel import gettext
-
-import config
-
-def get_help_menu_items():
-    """Return a (set) of dicts of help menu items, with name, priority, URL, 
-    target and onclick code."""
-    return [{'name': 'mnu_about',
-             'label': gettext('About %(appname)s', appname=config.APP_NAME), 
-             'priority': 999, 
-             'url': "#", 
-             'onclick': "about_show()"}]
-
-def get_scripts():
-    """Return a list of script URLs to include in the rendered page header"""
-    return [ url_for('about.script') ]
\ No newline at end of file
diff --git a/web/pgadmin/about/views.py b/web/pgadmin/about/views.py
deleted file mode 100644
index 0966a16..0000000
--- a/web/pgadmin/about/views.py
+++ /dev/null
@@ -1,48 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""A blueprint module implementing the about box."""
-MODULE_NAME = 'about'
-
-from flask import Blueprint, Response, current_app, render_template, __version__
-from flask.ext.babel import gettext
-from flask.ext.security import current_user, login_required
-
-import sys
-
-import config
-
-# Initialise the module
-blueprint = Blueprint(MODULE_NAME, __name__, static_folder='static',  static_url_path='', template_folder='templates', url_prefix='/' + MODULE_NAME)
-
-##########################################################################
-# A test page
-##########################################################################
-@blueprint.route("/")
-@login_required
-def index():
-    """Render the about box."""
-    info = { }
-    info['python_version'] = sys.version
-    info['flask_version'] = __version__
-    if config.SERVER_MODE == True:
-        info['app_mode'] = gettext('Server')
-    else:
-        info['app_mode'] = gettext('Desktop')
-    info['current_user'] = current_user.email
-    
-    return render_template(MODULE_NAME + '/index.html', info=info)
-    
-@blueprint.route("/about.js")
-@login_required
-def script():
-    """Render the required Javascript"""
-    return Response(response=render_template("about/about.js"),
-                    status=200,
-                    mimetype="application/javascript")
\ No newline at end of file
diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py
index 5442b7d..e340c83 100644
--- a/web/pgadmin/browser/__init__.py
+++ b/web/pgadmin/browser/__init__.py
@@ -6,7 +6,160 @@
 # This software is released under the PostgreSQL Licence
 #
 ##########################################################################
+from abc import ABCMeta, abstractmethod, abstractproperty
+from pgadmin import current_blueprint
+from pgadmin.utils import PgAdminModule
+from pgadmin.utils.ajax import make_json_response
+from pgadmin.settings import get_setting
+from flask import current_app, render_template, url_for, make_response
+from flask.ext.security import login_required
+from flask.ext.login import current_user
+from flaskext.gravatar import Gravatar
 
-# Define the node lists
-all_nodes = [ ]
-sub_nodes = [ ]
\ No newline at end of file
+MODULE_NAME = 'browser'
+
+class BrowserModule(PgAdminModule):
+
+
+
+    def get_own_stylesheets(self):
+        stylesheets = []
+        # Add browser stylesheets
+        for (endpoint, filename) in [
+            ('static', 'css/codemirror/codemirror.css'),
+            ('static', 'css/wcDocker/theme.css'),
+            ('static', 'css/jQuery-contextMenu/jquery.contextMenu.css'),
+            ('browser.static', 'css/browser.css'),
+            ('browser.static', 'css/aciTree/css/aciTree.css')
+            ]:
+            stylesheets.append(url_for(endpoint, filename=filename))
+        stylesheets.append(url_for('browser.browser_css'))
+        if current_app.debug:
+            stylesheets.append(url_for('static', filename='css/wcDocker/wcDockerSkeleton.css'))
+        else:
+            stylesheets.append(url_for('static', filename='css/wcDocker/wcDockerSkeleton.min.css'))
+        return stylesheets
+
+    def get_own_javascripts(self):
+        scripts = []
+        for (endpoint, filename) in [
+            ('static', 'js/codemirror/codemirror.js'),
+            ('static', 'js/codemirror/mode/sql.js'),
+            ('static', 'js/jQuery-contextMenu/jquery.ui.position.js'),
+            ('static', 'js/jQuery-contextMenu/jquery.contextMenu.js'),
+            ('browser.static', 'js/aciTree/jquery.aciPlugin.min.js'),
+            ('browser.static', 'js/aciTree/jquery.aciTree.dom.js'),
+            ('browser.static', 'js/aciTree/jquery.aciTree.min.js')]:
+            scripts.append(url_for(endpoint, filename=filename))
+        scripts.append(url_for('browser.browser_js'))
+        if current_app.debug:
+            scripts.append(url_for(
+                'static',
+                filename='js/wcDocker/wcDocker.js'))
+        else:
+            scripts.append(url_for(
+                'static',
+                filename='js/wcDocker/wcDocker.min.js'))
+        return scripts
+
+
+blueprint = BrowserModule(MODULE_NAME, __name__)
+
+class BrowserPluginModule(PgAdminModule):
+    """
+    Base class for browser submodules.
+    """
+
+    __metaclass__ = ABCMeta
+
+    def __init__(self, import_name, **kwargs):
+        kwargs.setdefault("url_prefix", self.node_path)
+        kwargs.setdefault("static_url_path", '')
+        super(BrowserPluginModule, self).__init__("NODE-%s" % self.node_type,
+                                            import_name,
+                                            **kwargs)
+
+
+    @property
+    @abstractmethod
+    def jssnippets(self):
+        """
+        Returns a snippet of javascript to include in the page
+        """
+        # TODO: move those methods to BrowserModule subclass ?
+        return []
+
+    @property
+    def csssnippets(self):
+        """
+        Returns a snippet of css to include in the page
+        """
+        # TODO: move those methods to BrowserModule subclass ?
+        return render_template("browser/css/node.css",
+                               node_type=self.node_type)
+
+    @abstractmethod
+    def get_nodes(self):
+        """
+        Each browser module is responsible for fetching
+        its own tree subnodes.
+        """
+        return []
+
+    @abstractproperty
+    def node_type(self):
+        pass
+
+    @property
+    def node_path(self):
+        return '/browser/nodes/' + self.node_type
+
+
+@blueprint.route("/")
+@login_required
+def index():
+    """Render and process the main browser window."""
+    # Get the Gravatar
+    gravatar = Gravatar(current_app,
+                        size=100,
+                        rating='g',
+                        default='retro',
+                        force_default=False,
+                        use_ssl=False,
+                        base_url=None)
+    return render_template(MODULE_NAME + "/index.html",
+                           username=current_user.email)
+
+@blueprint.route("/browser.js")
+@login_required
+def browser_js():
+    layout = get_setting('Browser/Layout', default='')
+    snippets = []
+    for submodule in current_blueprint.submodules:
+        snippets.extend(submodule.jssnippets)
+    return make_response(render_template(
+        'browser/js/browser.js',
+        layout=layout,
+        jssnippets=snippets),
+        200, {'Content-Type': 'application/x-javascript'})
+
+@blueprint.route("/browser.css")
+@login_required
+def browser_css():
+    """Render and return CSS snippets from the nodes and modules."""
+    snippets = []
+    for submodule in current_blueprint.submodules:
+        snippets.extend(submodule.csssnippets)
+    return make_response(render_template('browser/css/browser.css',
+                           snippets=snippets),
+                         200, {'Content-Type': 'text/css'})
+
+
+@blueprint.route("/nodes/")
+@login_required
+def get_nodes():
+    """Build a list of treeview nodes from the child nodes."""
+    nodes = []
+    for submodule in current_blueprint.submodules:
+        nodes.extend(submodule.get_nodes())
+    return make_json_response(data=nodes)
diff --git a/web/pgadmin/browser/hooks.py b/web/pgadmin/browser/hooks.py
deleted file mode 100644
index c382795..0000000
--- a/web/pgadmin/browser/hooks.py
+++ /dev/null
@@ -1,21 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""Browser application hooks"""
-
-import os, sys
-import config
-
-from pgadmin.browser.utils import register_modules
-from pgadmin.browser import all_nodes
-from . import sub_nodes
-
-def register_submodules(app):
-    """Register any child node blueprints"""
-    register_modules(app, __file__, all_nodes, sub_nodes, 'pgadmin.browser')
diff --git a/web/pgadmin/browser/server_groups/__init__.py b/web/pgadmin/browser/server_groups/__init__.py
index 95535f7..bfcc7f3 100644
--- a/web/pgadmin/browser/server_groups/__init__.py
+++ b/web/pgadmin/browser/server_groups/__init__.py
@@ -6,10 +6,202 @@
 # This software is released under the PostgreSQL Licence
 #
 ##########################################################################
+"""Defines views for management of server groups"""
 
-# Node meta data
-NODE_TYPE = 'server-group'
-NODE_PATH = '/browser/nodes/' + NODE_TYPE
+from abc import ABCMeta, abstractmethod
+import traceback
+from flask import Blueprint, Response, current_app, request, render_template
+from flask.ext.babel import gettext
+from flask.ext.security import current_user, login_required
+from pgadmin import current_blueprint
+from pgadmin.utils.ajax import make_json_response
+from pgadmin.browser import BrowserPluginModule
+from pgadmin.utils.menu import MenuItem
+from pgadmin.settings.settings_model import db, ServerGroup
+import config
 
-# Define the child node list
-sub_nodes = [ ]
\ No newline at end of file
+
+
+class ServerGroupModule(BrowserPluginModule):
+
+    NODE_TYPE = "server-group"
+
+    def get_own_menuitems(self):
+        return {
+            'standard_items': [
+                ServerGroupMenuItem(action="drop", priority=10, function="drop_server_group"),
+                ServerGroupMenuItem(action="rename", priority=10, function="rename_server_group")
+            ],
+            'create_items': [
+                ServerGroupMenuItem(name="create_server_group",
+                                    label=gettext('Server Group...'),
+                                    priority=10,
+                                    function="create_server_group",
+                                    types=[self.node_type])
+            ],
+            'context_items': [
+                ServerGroupMenuItem(name="delete_server_group",
+                                    label=gettext('Delete server group'),
+                                    priority=10,
+                                    onclick='drop_server_group(item);'),
+                ServerGroupMenuItem(name="rename_server_group",
+                                    label=gettext('Rename server group...'),
+                                    priority=10,
+                                    onclick='rename_server_group(item);')
+            ]
+        }
+
+
+    @property
+    def jssnippets(self):
+        snippets = [render_template("server_groups/server_groups.js")]
+        for module in self.submodules:
+            snippets.extend(module.jssnippets)
+        return snippets
+
+    def get_nodes(self, **kwargs):
+        """Return a JSON document listing the server groups for the user"""
+        groups = ServerGroup.query.filter_by(user_id=current_user.id)
+        # TODO: Move this JSON generation to a Server method
+        # this code is duplicated somewhere else
+        for group in groups:
+            yield {
+                "id": "%s/%d" % (self.node_type, group.id),
+                "label": group.name,
+                "icon": "icon-%s" % self.node_type,
+                "inode": True,
+                "_type": self.node_type
+            }
+
+    @property
+    def node_type(self):
+        return self.NODE_TYPE
+
+
+
+class ServerGroupMenuItem(MenuItem):
+
+    def __init__(self, **kwargs):
+        kwargs.setdefault("type", ServerGroupModule.NODE_TYPE)
+        super(ServerGroupMenuItem, self).__init__(**kwargs)
+
+
+class ServerGroupPluginModule(BrowserPluginModule):
+    """
+    Base class for server group plugins.
+    """
+
+    __metaclass__ = ABCMeta
+
+
+    @abstractmethod
+    def get_nodes(self, servergroup):
+        pass
+
+
+# Initialise the module
+blueprint = ServerGroupModule( __name__, static_url_path='')
+
+@blueprint.route("/<server_group>")
+@login_required
+def get_nodes(server_group):
+    """Build a list of treeview nodes from the child nodes."""
+    nodes = []
+    for module in current_blueprint.submodules:
+        nodes.extend(module.get_nodes(server_group=server_group))
+    return make_json_response(data=nodes)
+
+
+@blueprint.route('/add/', methods=['POST'])
+@login_required
+def add():
+    """Add a server group node to the settings database"""
+    success = 1
+    errormsg = ''
+    data = { }
+
+    if request.form['name'] != '':
+        servergroup = ServerGroup(user_id=current_user.id, name=request.form['name'])
+
+        try:
+            db.session.add(servergroup)
+            db.session.commit()
+        except Exception as e:
+            success = 0
+            errormsg = e.message
+
+    else:
+        success = 0
+        errormsg = gettext('No server group name was specified')
+
+    if success == 1:
+        data['id'] = servergroup.id
+        data['name'] = servergroup.name
+
+    return make_json_response(success=success,
+                              errormsg=errormsg,
+                              info=traceback.format_exc(),
+                              result=request.form,
+                              data=data)
+
+@blueprint.route('/delete/', methods=['POST'])
+@login_required
+def delete():
+    """Delete a server group node in the settings database"""
+    success = 1
+    errormsg = ''
+
+    if request.form['id'] != '':
+        # There can be only one record at most
+        servergroup = ServerGroup.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first()
+
+        if servergroup is None:
+            success = 0
+            errormsg = gettext('The specified server group could not be found.')
+        else:
+            try:
+                db.session.delete(servergroup)
+                db.session.commit()
+            except Exception as e:
+                success = 0
+                errormsg = e.message
+
+    else:
+        success = 0
+        errormsg = gettext('No server group  was specified.')
+
+    return make_json_response(success=success,
+                              errormsg=errormsg,
+                              info=traceback.format_exc(),
+                              result=request.form)
+
+@blueprint.route('/rename/', methods=['POST'])
+@login_required
+def rename():
+    """Rename a server group node in the settings database"""
+    success = 1
+    errormsg = ''
+
+    if request.form['id'] != '':
+        # There can be only one record at most
+        servergroup = ServerGroup.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first()
+
+        if servergroup is None:
+            success = 0
+            errormsg = gettext('The specified server group could not be found.')
+        else:
+            try:
+                servergroup.name = request.form['name']
+                db.session.commit()
+            except Exception as e:
+                success = 0
+                errormsg = e.message
+
+    else:
+        success = 0
+        errormsg = gettext('No server group was specified.')
+
+    return make_json_response(success=success,
+                              errormsg=errormsg,
+                              info=traceback.format_exc(),
+                              result=request.form)
diff --git a/web/pgadmin/browser/server_groups/hooks.py b/web/pgadmin/browser/server_groups/hooks.py
deleted file mode 100644
index 413e093..0000000
--- a/web/pgadmin/browser/server_groups/hooks.py
+++ /dev/null
@@ -1,78 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""Integration hooks for server groups."""
-
-from flask import render_template, url_for
-from flask.ext.babel import gettext
-from flask.ext.security import current_user
-
-from pgadmin.browser.utils import register_modules
-from pgadmin.settings.settings_model import db, ServerGroup
-
-from pgadmin.browser import all_nodes
-from . import NODE_TYPE, sub_nodes
-
-def register_submodules(app):
-    """Register any child node blueprints"""
-    register_modules(app, __file__, all_nodes, sub_nodes, 'pgadmin.browser.server_groups')
-
-def get_nodes():
-    """Return a JSON document listing the server groups for the user"""
-    groups = ServerGroup.query.filter_by(user_id=current_user.id)
-    # TODO: Move this JSON generation to a Server method
-    # this code is duplicated somewhere else
-    for group in groups:
-        yield {
-            "id": "%s/%d" % (NODE_TYPE, group.id),
-            "label": group.name,
-            "icon": "icon-%s" % NODE_TYPE,
-            "inode": True,
-            "_type": NODE_TYPE
-        }
-
-def get_standard_menu_items():
-    """Return a (set) of dicts of standard menu items (create/drop/rename), with
-    object type, action and the function name (no parens) to call on click."""
-    return [
-            {'type': 'server-group', 'action': 'drop', 'priority': 10, 'function': 'drop_server_group'},
-            {'type': 'server-group', 'action': 'rename', 'priority': 20, 'function': 'rename_server_group'}
-           ]
-
-
-def get_create_menu_items():
-    """Return a (set) of dicts of create menu items, with a Javascript array of
-    object types on which the option should appear, name, label and the function
-    name (no parens) to call on click."""
-    return [
-            {'type': "['server-group']", 'name': 'create_server_group', 'label': gettext('Server Group...'), 'priority': 10, 'function': 'create_server_group'}
-           ]
-
-
-def get_context_menu_items():
-    """Return a (set) of dicts of content menu items with name, node type, label, priority and JS"""
-    return [
-            {'name': 'delete_server_group', 'type': NODE_TYPE, 'label': gettext('Delete server group'), 'priority': 10, 'onclick': 'drop_server_group(item);'},
-            {'name': 'rename_server_group', 'type': NODE_TYPE, 'label': gettext('Rename server group...'), 'priority': 20, 'onclick': 'rename_server_group(item);'}
-           ]
-
-
-def get_script_snippets():
-    """Return the script snippets needed to handle treeview node operations."""
-    return render_template('server_groups/server_groups.js')
-
-
-def get_css_snippets():
-    """Return the CSS needed to display the treeview node image."""
-    css = ".icon-server-group {\n"
-    css += " background: url('%s') 0 0 no-repeat !important;\n" % \
-            url_for('NODE-%s.static' % NODE_TYPE, filename='img/server-group.png')
-    css += "}\n"
-
-    return css
diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py
index a9e5e3c..855bb19 100644
--- a/web/pgadmin/browser/server_groups/servers/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/__init__.py
@@ -6,11 +6,167 @@
 # This software is released under the PostgreSQL Licence
 #
 ##########################################################################
+from flask import render_template, request
+from pgadmin.browser.server_groups import ServerGroupPluginModule
+from flask.ext.security import login_required, current_user
+from pgadmin.settings.settings_model import db, Server
+from pgadmin.utils.menu import MenuItem
+from pgadmin.utils.ajax import make_json_response
+import traceback
+from flask.ext.babel import gettext
 
-# Node meta data
-NODE_TYPE = 'server'
-NODE_PATH = '/browser/nodes/' + NODE_TYPE
 
-# Define the child node list
-sub_nodes = [ ]
+class ServerModule(ServerGroupPluginModule):
+
+    NODE_TYPE = "server"
+
+    @property
+    def node_type(self):
+        return self.NODE_TYPE
+
+    def get_nodes(self, server_group):
+        """Return a JSON document listing the server groups for the user"""
+        servers = Server.query.filter_by(user_id=current_user.id, servergroup_id=server_group)
+
+        # TODO: Move this JSON generation to a Server method
+        for server in servers:
+            yield {
+                "id": "%s/%d" % (NODE_TYPE, server.id),
+                "label": server.name,
+                "icon": "icon-%s" % NODE_TYPE,
+                "inode": True,
+                "_type": NODE_TYPE
+            }
+
+    def get_own_menuitems(self):
+        return {
+            'standard_items': [
+                ServerMenuItem(action="drop", priority=50, function='drop_server'),
+                ServerMenuItem(action="rename", priority=50, function='rename_server')
+            ],
+            'create_items': [
+                ServerMenuItem(types=["server-group", "server"],
+                               name="create_server",
+                               label=gettext('Server...'),
+                               priority=50,
+                               function='create_server')
+            ],
+            'context_items': [
+                ServerMenuItem(name='delete_server',
+                               label=gettext('Delete server'),
+                               priority=50,
+                               onclick='drop_server'),
+                ServerMenuItem(name='rename_server',
+                               label=gettext('Rename server...'),
+                               priority=60,
+                               onclick='rename_server(item);')
+            ]
+        }
+
+
+    @property
+    def jssnippets(self):
+        return [render_template("servers/servers.js")]
+
+
+class ServerMenuItem(MenuItem):
+
+    def __init__(self, **kwargs):
+        kwargs.setdefault("type", ServerModule.NODE_TYPE)
+        super(ServerMenuItem, self).__init__(**kwargs)
+
+blueprint = ServerModule(__name__)
+
+@blueprint.route('/add/', methods=['POST'])
+@login_required
+def add():
+    """Add a server node to the settings database"""
+    success = 1
+    errormsg = ''
+    data = {}
+
+    success = False
+    errormsg = ''
+    if request.form['name'] != '':
+        server = Server(user_id=current_user.id, name=request.form['name'])
+        try:
+            db.session.add(server)
+            db.session.commit()
+            success = True
+        except Exception as e:
+            errormsg = e.message
+    else:
+        errormsg = gettext('No server name was specified')
+
+    if success:
+        data['id'] = server.id
+        data['name'] = server.name
+
+    return make_json_response(success=success,
+                              errormsg=errormsg,
+                              info=traceback.format_exc(),
+                              result=request.form,
+                              data=data)
+
+@blueprint.route('/delete/', methods=['POST'])
+@login_required
+def delete():
+    """Delete a server node in the settings database"""
+    success = 1
+    errormsg = ''
+
+    if request.form['id'] != '':
+        # There can be only one record at most
+        servergroup = Server.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first()
+
+        if server is None:
+            success = 0
+            errormsg = gettext('The specified server could not be found.')
+        else:
+            try:
+                db.session.delete(server)
+                db.session.commit()
+            except Exception as e:
+                success = 0
+                errormsg = e.message
+
+    else:
+        success = 0
+        errormsg = gettext('No server was specified.')
+
+    return make_json_response(success=success,
+                              errormsg=errormsg,
+                              info=traceback.format_exc(),
+                              result=request.form)
+
+@blueprint.route('/rename/', methods=['POST'])
+@login_required
+def rename():
+    """Rename a server node in the settings database"""
+    success = 1
+    errormsg = ''
+
+    if request.form['id'] != '':
+        # There can be only one record at most
+        servergroup = Server.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first()
+
+        if server is None:
+            success = 0
+            errormsg = gettext('The specified server could not be found.')
+        else:
+            try:
+                server.name = request.form['name']
+                db.session.commit()
+            except Exception as e:
+                success = 0
+                errormsg = e.message
+
+    else:
+        success = 0
+        errormsg = gettext('No server was specified.')
+
+    return make_json_response(success=success,
+                              errormsg=errormsg,
+                              info=traceback.format_exc(),
+                              result=request.form)
 
diff --git a/web/pgadmin/browser/server_groups/servers/hooks.py b/web/pgadmin/browser/server_groups/servers/hooks.py
deleted file mode 100644
index 30a7c5f..0000000
--- a/web/pgadmin/browser/server_groups/servers/hooks.py
+++ /dev/null
@@ -1,72 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""Integration hooks for servers."""
-
-from flask import render_template, url_for
-from flask.ext.babel import gettext
-from flask.ext.security import current_user
-
-from pgadmin.settings.settings_model import db, Server
-from . import NODE_TYPE
-
-def get_nodes(server_group):
-    """Return a JSON document listing the server groups for the user"""
-    servers = Server.query.filter_by(user_id=current_user.id, servergroup_id=server_group)
-
-    # TODO: Move this JSON generation to a Server method
-    for server in servers:
-        yield {
-            "id": "%s/%d" % (NODE_TYPE, server.id),
-            "label": server.name,
-            "icon": "icon-%s" % NODE_TYPE,
-            "inode": True,
-            "_type": NODE_TYPE
-        }
-
-
-def get_standard_menu_items():
-    """Return a (set) of dicts of standard menu items (create/drop/rename), with
-    object type, action, priority and the function to call on click."""
-    return [
-            {'type': 'server', 'action': 'drop', 'priority': 50, 'function': 'drop_server'},
-            {'type': 'server', 'action': 'rename', 'priority': 60, 'function': 'rename_server'}
-           ]
-
-
-def get_create_menu_items():
-    """Return a (set) of dicts of create menu items, with a Javascript array of
-    object types on which the option should appear, name, label, priority and
-    the function name (no parens) to call on click."""
-    return [
-            {'type': "['server-group', 'server']", 'name': 'create_server', 'label': gettext('Server...'), 'priority': 50, 'function': 'create_server'}
-           ]
-
-
-def get_context_menu_items():
-    """Return a (set) of dicts of content menu items with name, node type, label, priority and JS"""
-    return [
-            {'name': 'delete_server', 'type': NODE_TYPE, 'label': gettext('Delete server'), 'priority': 50, 'onclick': 'drop_server(item);'},
-            {'name': 'rename_server', 'type': NODE_TYPE, 'label': gettext('Rename server...'), 'priority': 60, 'onclick': 'rename_server(item);'}
-           ]
-
-
-def get_script_snippets():
-    """Return the script snippets needed to handle treeview node operations."""
-    return render_template('servers/servers.js')
-
-
-def get_css_snippets():
-    """Return the CSS needed to display the treeview node image."""
-    css = ".icon-server {\n"
-    css += " background: url('%s') 0 0 no-repeat !important;\n" % \
-            url_for('NODE-%s.static' % NODE_TYPE, filename='img/server.png')
-    css += "}\n"
-
-    return css
diff --git a/web/pgadmin/browser/server_groups/servers/views.py b/web/pgadmin/browser/server_groups/servers/views.py
deleted file mode 100644
index fb38dcd..0000000
--- a/web/pgadmin/browser/server_groups/servers/views.py
+++ /dev/null
@@ -1,121 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""Defines views for management of servers"""
-
-from flask import Blueprint, request
-from flask.ext.babel import gettext
-from flask.ext.security import current_user, login_required
-
-from . import NODE_TYPE, NODE_PATH
-from pgadmin.utils.ajax import make_json_response
-from pgadmin.settings.settings_model import db, Server
-import traceback
-
-# Initialise the module
-blueprint = Blueprint("NODE-" + NODE_TYPE, __name__,
-                      static_folder='static',
-                      static_url_path='',
-                      template_folder='templates',
-                      url_prefix=NODE_PATH)
-
-
-@blueprint.route('/add/', methods=['POST'])
-@login_required
-def add():
-    """Add a server node to the settings database"""
-    success = 1
-    errormsg = ''
-    data = {}
-
-    success = False
-    errormsg = ''
-    if request.form['name'] != '':
-        server = Server(user_id=current_user.id, name=request.form['name'])
-        try:
-            db.session.add(server)
-            db.session.commit()
-            success = True
-        except Exception as e:
-            errormsg = e.message
-    else:
-        errormsg = gettext('No server name was specified')
-
-    if success:
-        data['id'] = server.id
-        data['name'] = server.name
-
-    return make_json_response(success=success,
-                              errormsg=errormsg,
-                              info=traceback.format_exc(),
-                              result=request.form,
-                              data=data)
-
-@blueprint.route('/delete/', methods=['POST'])
-@login_required
-def delete():
-    """Delete a server node in the settings database"""
-    success = 1
-    errormsg = ''
-
-    if request.form['id'] != '':
-        # There can be only one record at most
-        servergroup = Server.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first()
-
-        if server is None:
-            success = 0
-            errormsg = gettext('The specified server could not be found.')
-        else:
-            try:
-                db.session.delete(server)
-                db.session.commit()
-            except Exception as e:
-                success = 0
-                errormsg = e.message
-
-    else:
-        success = 0
-        errormsg = gettext('No server was specified.')
-
-    return make_json_response(success=success,
-                              errormsg=errormsg,
-                              info=traceback.format_exc(),
-                              result=request.form)
-
-@blueprint.route('/rename/', methods=['POST'])
-@login_required
-def rename():
-    """Rename a server node in the settings database"""
-    success = 1
-    errormsg = ''
-
-    if request.form['id'] != '':
-        # There can be only one record at most
-        servergroup = Server.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first()
-
-        if server is None:
-            success = 0
-            errormsg = gettext('The specified server could not be found.')
-        else:
-            try:
-                server.name = request.form['name']
-                db.session.commit()
-            except Exception as e:
-                success = 0
-                errormsg = e.message
-
-    else:
-        success = 0
-        errormsg = gettext('No server was specified.')
-
-    return make_json_response(success=success,
-                              errormsg=errormsg,
-                              info=traceback.format_exc(),
-                              result=request.form)
-
diff --git a/web/pgadmin/browser/server_groups/templates/server_groups/server_groups.js b/web/pgadmin/browser/server_groups/templates/server_groups/server_groups.js
index 29b5449..f1fa85d 100644
--- a/web/pgadmin/browser/server_groups/templates/server_groups/server_groups.js
+++ b/web/pgadmin/browser/server_groups/templates/server_groups/server_groups.js
@@ -79,4 +79,4 @@ function rename_server_group(item) {
         },
         null
     )
-}
\ No newline at end of file
+}
diff --git a/web/pgadmin/browser/server_groups/views.py b/web/pgadmin/browser/server_groups/views.py
deleted file mode 100644
index 5348819..0000000
--- a/web/pgadmin/browser/server_groups/views.py
+++ /dev/null
@@ -1,129 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""Defines views for management of server groups"""
-
-import traceback
-from flask import Blueprint, Response, current_app, request
-from flask.ext.babel import gettext
-from flask.ext.security import current_user, login_required
-
-from . import NODE_TYPE, NODE_PATH, sub_nodes
-from pgadmin.utils.ajax import make_json_response
-from pgadmin.settings.settings_model import db, ServerGroup
-import config
-
-# Initialise the module
-blueprint = Blueprint("NODE-" + NODE_TYPE, __name__, static_folder='static',  static_url_path='', template_folder='templates', url_prefix=NODE_PATH)
-
-@blueprint.route("/<server_group>")
-@login_required
-def get_nodes(server_group):
-    """Build a list of treeview nodes from the child nodes."""
-    nodes = []
-    for node in sub_nodes:
-        if hasattr(node, 'hooks') and hasattr(node.hooks, 'get_nodes'):
-            nodes.extend(node.hooks.get_nodes(server_group))
-    return make_json_response(data=nodes)
-
-
-@blueprint.route('/add/', methods=['POST'])
-@login_required
-def add():
-    """Add a server group node to the settings database"""
-    success = 1
-    errormsg = ''
-    data = { }
-
-    if request.form['name'] != '':
-        servergroup = ServerGroup(user_id=current_user.id, name=request.form['name'])
-
-        try:
-            db.session.add(servergroup)
-            db.session.commit()
-        except Exception as e:
-            success = 0
-            errormsg = e.message
-
-    else:
-        success = 0
-        errormsg = gettext('No server group name was specified')
-
-    if success == 1:
-        data['id'] = servergroup.id
-        data['name'] = servergroup.name
-
-    return make_json_response(success=success,
-                              errormsg=errormsg,
-                              info=traceback.format_exc(),
-                              result=request.form,
-                              data=data)
-
-@blueprint.route('/delete/', methods=['POST'])
-@login_required
-def delete():
-    """Delete a server group node in the settings database"""
-    success = 1
-    errormsg = ''
-
-    if request.form['id'] != '':
-        # There can be only one record at most
-        servergroup = ServerGroup.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first()
-
-        if servergroup is None:
-            success = 0
-            errormsg = gettext('The specified server group could not be found.')
-        else:
-            try:
-                db.session.delete(servergroup)
-                db.session.commit()
-            except Exception as e:
-                success = 0
-                errormsg = e.message
-
-    else:
-        success = 0
-        errormsg = gettext('No server group  was specified.')
-
-    return make_json_response(success=success,
-                              errormsg=errormsg,
-                              info=traceback.format_exc(),
-                              result=request.form)
-
-@blueprint.route('/rename/', methods=['POST'])
-@login_required
-def rename():
-    """Rename a server group node in the settings database"""
-    success = 1
-    errormsg = ''
-
-    if request.form['id'] != '':
-        # There can be only one record at most
-        servergroup = ServerGroup.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first()
-
-        if servergroup is None:
-            success = 0
-            errormsg = gettext('The specified server group could not be found.')
-        else:
-            try:
-                servergroup.name = request.form['name']
-                db.session.commit()
-            except Exception as e:
-                success = 0
-                errormsg = e.message
-
-    else:
-        success = 0
-        errormsg = gettext('No server group was specified.')
-
-    return make_json_response(success=success,
-                              errormsg=errormsg,
-                              info=traceback.format_exc(),
-                              result=request.form)
-
diff --git a/web/pgadmin/browser/templates/browser/css/browser.css b/web/pgadmin/browser/templates/browser/css/browser.css
new file mode 100644
index 0000000..9fb9ed4
--- /dev/null
+++ b/web/pgadmin/browser/templates/browser/css/browser.css
@@ -0,0 +1,3 @@
+{% for snip in snippets %}
+{{ snip }}
+{% endfor %}
diff --git a/web/pgadmin/browser/templates/browser/css/node.css b/web/pgadmin/browser/templates/browser/css/node.css
new file mode 100644
index 0000000..3fea074
--- /dev/null
+++ b/web/pgadmin/browser/templates/browser/css/node.css
@@ -0,0 +1,3 @@
+.icon-{{node_type}} {
+  background: url('{{ url_for('NODE-%s.static' % node_type, filename='img/%s.png' % node_type )}}') 0 0 no-repeat;
+}
diff --git a/web/pgadmin/browser/templates/browser/index.html b/web/pgadmin/browser/templates/browser/index.html
index 481d268..dd3a4b2 100644
--- a/web/pgadmin/browser/templates/browser/index.html
+++ b/web/pgadmin/browser/templates/browser/index.html
@@ -31,39 +31,22 @@
             <li><a id="mnu_drop_object" href="#" onclick="drop_object()">{{ _('Drop object') }}</a></li>
             <li><a id="mnu_rename_object" href="#" onclick="rename_object()">{{ _('Rename object') }}</a></li>
             <li class="divider"></li>
-            {% if file_items is defined and file_items|count > 0 %}{% for file_item in file_items %}
-            <li><a id="{{ file_item.name }}" href="{{ file_item.url }}"{% if file_item.target %} target="{{ file_item.target }}"{% endif %}{% if file_item.onclick %} onclick="{{ file_item.onclick|safe }}"{% endif %}>{{ file_item.label }}</a></li>{% endfor %}{% endif %}
+            {% for file_item in menu_items.file_items %}
+            <li><a id="{{ file_item.name }}" href="{{ file_item.url }}"{% if file_item.target %} target="{{ file_item.target }}"{% endif %}{% if file_item.onclick %} onclick="{{ file_item.onclick|safe }}"{% endif %}>{{ file_item.label }}</a></li>{% endfor %}
           </ul>
         </li>
-
-        {% if edit_items is defined and edit_items|count > 0 %}<li class="dropdown">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ _('Edit') }} <span class="caret"></span></a>
-          <ul class="dropdown-menu" role="menu">{% for edit_item in edit_items %}
-            <li><a id="{{ edit_item.name }}" href="{{ edit_item.url }}"{% if edit_item.target %} target="{{ edit_item.target }}"{% endif %}{% if edit_item.onclick %} onclick="{{ edit_item.onclick|safe }}"{% endif %}>{{ edit_item.label }}</a></li>{% endfor %}
-          </ul>
-        </li>{% endif %}
-        
-        {% if tools_items is defined and tools_items|count > 0 %}<li class="dropdown">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ _('Tools') }} <span class="caret"></span></a>
-          <ul class="dropdown-menu" role="menu">{% for tools_item in tools_items %}
-            <li><a id="{{ tools_item.name }}" href="{{ tools_item.url }}"{% if tools_item.target %} target="{{ tools_item.target }}"{% endif %}{% if tools_item.onclick %} onclick="{{ tools_item.onclick|safe }}"{% endif %}>{{ tools_item.label }}</a></li>{% endfor %}
-          </ul>
-        </li>{% endif %}
-        
-        {% if management_items is defined and management_items|count > 0 %}<li class="dropdown">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ _('Management') }} <span class="caret"></span></a>
-          <ul class="dropdown-menu" role="menu">{% for management_item in management_items %}
-            <li><a id="{{ management_item.name }}" href="{{ management_item.url }}"{% if management_item.target %} target="{{ management_item.target }}"{% endif %}{% if management_item.onclick %} onclick="{{ management_item.onclick|safe }}"{% endif %}>{{ management_item.label }}</a></li>{% endfor %}
-          </ul>
-        </li>{% endif %}
-        
-        {% if help_items is defined and help_items|count > 0 %}<li class="dropdown">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ _('Help') }} <span class="caret"></span></a>
-          <ul class="dropdown-menu" role="menu">{% for help_item in help_items %}
-            <li><a id="{{ help_item.name }}" href="{{ help_item.url }}"{% if help_item.target %} target="{{ help_item.target }}"{% endif %}{% if help_item.onclick %} onclick="{{ help_item.onclick|safe }}"{% endif %}>{{ help_item.label }}</a></li>{% endfor %}
-          </ul>
-        </li>{% endif %}
-
+        {% for key in ('Edit', 'Tools', 'Management', 'Help') %}
+          {% if menu_items['%s_items' % key.lower()] %}
+        <li class="dropdown">
+          <a href="#" class="dropdown-toggle" data-toggle="dropdown"
+            role="button" aria-expanded="false">{{ _(key) }} <span class="caret"></span></a>
+            <ul class="dropdown-menu" role="menu">
+              {% for item in menu_items['%s_items' % key.lower()] %}
+              <li><a id="{{ item.name }}" href="{{ item.url }}"{% if item.target %} target="{{ item.target }}"{% endif %}{% if item.onclick %} onclick="{{ item.onclick|safe }}"{% endif %}>{{ item.label }}</a></li>
+              {% endfor %}
+            </ul>
+        </li>{%endif%}
+        {% endfor %}
       </ul>
       {% if config.SERVER_MODE %}
       <ul class="nav navbar-nav navbar-right">
diff --git a/web/pgadmin/browser/templates/browser/js/browser.js b/web/pgadmin/browser/templates/browser/js/browser.js
index a5c324d..e1aa05b 100644
--- a/web/pgadmin/browser/templates/browser/js/browser.js
+++ b/web/pgadmin/browser/templates/browser/js/browser.js
@@ -1,21 +1,21 @@
 // Page globals
-var docker
-var editor
-var tree
-var dashboardPanel
-var propertiesPanel
-var statisticsPanel
-var dependenciesPanel
-var dependentsPanel
-var sqlPanel
-var browserPanel
+var docker;
+var editor;
+var tree;
+var dashboardPanel;
+var propertiesPanel;
+var statisticsPanel;
+var dependenciesPanel;
+var dependentsPanel;
+var sqlPanel;
+var browserPanel;
 
 // Store the main browser layout
-$(window).bind('unload', function() { 
+$(window).bind('unload', function() {
     state = docker.save();
-    settings = { setting: "Browser/Layout", 
+    settings = { setting: "Browser/Layout",
                  value: state }
-    
+
     $.post("{{ url_for('settings.store') }}", settings);
 
     return true
@@ -28,8 +28,8 @@ function buildPanel(docker, name, title, width, height, showTitle, isCloseable,
         isPrivate: isPrivate,
         onCreate: function(myPanel) {
             myPanel.initSize(width, height);
-            
-            if (showTitle == false) 
+
+            if (showTitle == false)
                 myPanel.title(false);
 
             myPanel.closeable(isCloseable);
@@ -46,8 +46,8 @@ function buildIFramePanel(docker, name, title, width, height, showTitle, isClose
         isPrivate: isPrivate,
         onCreate: function(myPanel) {
             myPanel.initSize(width, height);
-            
-            if (showTitle == false) 
+
+            if (showTitle == false)
                 myPanel.title(false);
 
             myPanel.closeable(isCloseable);
@@ -87,7 +87,7 @@ function report_error(message, info) {
       <div class="panel-body" style="overflow: scroll;">' + message + '</div>\
     </div>\
   </div>'
-  
+
     if (info != null && info != '') {
         text += '<div class="panel panel-default">\
     <div class="panel-heading" role="tab" id="headingTwo">\
@@ -103,9 +103,9 @@ function report_error(message, info) {
   </div>\
 </div>'
     }
-    
+
     text += '</div>'
-    
+
     alertify.alert(
         '{{ _('An error has occurred') }}',
         text
@@ -115,35 +115,35 @@ function report_error(message, info) {
 
 // Enable/disable menu options
 function enable_disable_menus() {
-    
+
     // Disable everything first
     $("#mnu_create").html('<li class="menu-item disabled"><a href="#">{{ _('No object selected') }}</a></li>\n');
-    $("#mnu_drop_object").addClass("mnu-disabled"); 
-    $("#mnu_rename_object").addClass("mnu-disabled"); 
+    $("#mnu_drop_object").addClass("mnu-disabled");
+    $("#mnu_rename_object").addClass("mnu-disabled");
     node_type = get_selected_node_type()
-    
+
     // List the possible standard items, their types and actions
-    var handlers = [{% if standard_items is defined %}{% for standard_item in standard_items %}
-        "{{ standard_item.type }}:{{ standard_item.action }}",{% endfor %}{% endif %} 
+    var handlers = [{% for standard_item in menu_items.standard_items %}
+        "{{ standard_item.type }}:{{ standard_item.action }}",{% endfor %}
     ]
-    
+
     // Check if we have a matching action for the object type in the list, and
     // if so, enable the menu item
     if ($.inArray(node_type + ":drop", handlers) >= 0)
-        $("#mnu_drop_object").removeClass("mnu-disabled"); 
-        
+        $("#mnu_drop_object").removeClass("mnu-disabled");
+
     if ($.inArray(node_type + ":rename", handlers) >= 0)
         $("#mnu_rename_object").removeClass("mnu-disabled");
-        
+
     // List the possibe create items
-    var creators = [{% if create_items is defined %}{% for create_item in create_items %}
-        [{{ create_item.type }}, "{{ create_item.name }}", "{{ create_item.label }}", "{{ create_item.function }}"],{% endfor %}{% endif %} 
+    var creators = [{% for create_item in menu_items.create_items %}
+        [{{ create_item.types | tojson }}, "{{ create_item.name }}", "{{ create_item.label }}", "{{ create_item.function }}"],{% endfor %}
     ]
-    
+
     // Loop through the list of creators and add links for any that apply to this
     // node type to the Create menu's UL element
     items = ''
-    
+
     for (i = 0; i < creators.length; ++i) {
         if ($.inArray(node_type, creators[i][0]) >= 0) {
             items = items + '<li class="menu-item"><a href="#" onclick="' + creators[i][3] + '()">' + creators[i][2] + '</a></li>\n'
@@ -158,22 +158,22 @@ function get_selected_node_type() {
     item = tree.selected()
     if (!item || item.length != 1)
         return "";
-        
+
     return tree.itemData(tree.selected())._type;
 }
-    
+
 // Create a new object of the type currently selected
 function create_object() {
     node_type = get_selected_node_type()
     if (node_type == "")
         return;
-    
+
     switch(node_type) {
-    {% if standard_items is defined %}{% for standard_item in standard_items %}{% if standard_item.action == 'create' %}
+    {% for standard_item in menu_items.standard_items %}{% if standard_item.action == 'create' %}
         case '{{ standard_item.type }}':
              {{ standard_item.function }}()
              break;
-    {% endif %}{% endfor %}{% endif %} 
+    {% endif %}{% endfor %}
     }
 }
 
@@ -182,13 +182,13 @@ function drop_object() {
     node_type = get_selected_node_type()
     if (node_type == "")
         return;
-    
+
     switch(node_type) {
-    {% if standard_items is defined %}{% for standard_item in standard_items %}{% if standard_item.action == 'drop' %}
+    {% for standard_item in menu_items.standard_items %}{% if standard_item.action == 'drop' %}
         case '{{ standard_item.type }}':
              {{ standard_item.function }}(tree.selected())
              break;
-    {% endif %}{% endfor %}{% endif %} 
+    {% endif %}{% endfor %}
     }
 }
 
@@ -197,13 +197,13 @@ function rename_object() {
     node_type = get_selected_node_type()
     if (node_type == "")
         return;
-    
+
     switch(node_type) {
-    {% if standard_items is defined %}{% for standard_item in standard_items %}{% if standard_item.action == 'rename' %}
+    {% for standard_item in menu_items.standard_items %}{% if standard_item.action == 'rename' %}
         case '{{ standard_item.type }}':
              {{ standard_item.function }}(tree.selected())
              break;
-    {% endif %}{% endfor %}{% endif %} 
+    {% endif %}{% endfor %}
     }
 }
 
@@ -237,37 +237,37 @@ WITH ( \n\
 ); \n\
 ALTER TABLE tickets_detail \n\
   OWNER TO helpdesk;\n';
-  
-        buildPanel(docker, 'pnl_browser', '{{ _('Browser') }}', 300, 600, false, false, true, 
+
+        buildPanel(docker, 'pnl_browser', '{{ _('Browser') }}', 300, 600, false, false, true,
                   '<div id="tree" class="aciTree">')
-        buildIFramePanel(docker, 'pnl_dashboard', '{{ _('Dashboard') }}', 500, 600, true, false, true, 
+        buildIFramePanel(docker, 'pnl_dashboard', '{{ _('Dashboard') }}', 500, 600, true, false, true,
                   'http://www.pgadmin.org/')
-        buildPanel(docker, 'pnl_properties', '{{ _('Properties') }}', 500, 600, true, false, true, 
+        buildPanel(docker, 'pnl_properties', '{{ _('Properties') }}', 500, 600, true, false, true,
                   '<p>Properties pane</p>')
-        buildPanel(docker, 'pnl_sql', '{{ _('SQL') }}', 500, 600, true, false, true, 
+        buildPanel(docker, 'pnl_sql', '{{ _('SQL') }}', 500, 600, true, false, true,
                   '<textarea id="sql-textarea" name="sql-textarea">' + demoSql + '</textarea>')
-        buildPanel(docker, 'pnl_statistics', '{{ _('Statistics') }}', 500, 600, true, false, true, 
+        buildPanel(docker, 'pnl_statistics', '{{ _('Statistics') }}', 500, 600, true, false, true,
                   '<p>Statistics pane</p>')
-        buildPanel(docker, 'pnl_dependencies', '{{ _('Dependencies') }}', 500, 600, true, false, true, 
+        buildPanel(docker, 'pnl_dependencies', '{{ _('Dependencies') }}', 500, 600, true, false, true,
                   '<p>Depedencies pane</p>')
-        buildPanel(docker, 'pnl_dependents', '{{ _('Dependents') }}', 500, 600, true, false, true, 
+        buildPanel(docker, 'pnl_dependents', '{{ _('Dependents') }}', 500, 600, true, false, true,
                   '<p>Dependents pane</p>')
-        
+
         // Add hooked-in panels
-        {% if panel_items is defined and panel_items|count > 0 %}{% for panel_item in panel_items %}{% if panel_item.isIframe %}
-        buildIFramePanel(docker, '{{ panel_item.name }}', '{{ panel_item.title }}', 
-                                  {{ panel_item.width }}, {{ panel_item.height }}, 
-                                  {{ panel_item.showTitle|lower }}, {{ panel_item.isCloseable|lower }}, 
+        {% for panel_item in current_app.panels %}{% if panel_item.isIframe %}
+        buildIFramePanel(docker, '{{ panel_item.name }}', '{{ panel_item.title }}',
+                                  {{ panel_item.width }}, {{ panel_item.height }},
+                                  {{ panel_item.showTitle|lower }}, {{ panel_item.isCloseable|lower }},
                                   {{ panel_item.isPrivate|lower }}, '{{ panel_item.content }}')
         {% else %}
-        buildPanel(docker, '{{ panel_item.name }}', '{{ panel_item.title }}', 
-                            {{ panel_item.width }}, {{ panel_item.height }}, 
-                            {{ panel_item.showTitle|lower }}, {{ panel_item.isCloseable|lower }}, 
-                            {{ panel_item.isPrivate|lower }}, '{{ panel_item.content }}')                         
-        {% endif %}{% endfor %}{% endif %}
-        
+        buildPanel(docker, '{{ panel_item.name }}', '{{ panel_item.title }}',
+                            {{ panel_item.width }}, {{ panel_item.height }},
+                            {{ panel_item.showTitle|lower }}, {{ panel_item.isCloseable|lower }},
+                            {{ panel_item.isPrivate|lower }}, '{{ panel_item.content }}')
+        {% endif %}{% endfor %}
+
         var layout = '{{ layout }}';
-        
+
         // Try to restore the layout if there is one
         if (layout != '') {
             try {
@@ -281,7 +281,7 @@ ALTER TABLE tickets_detail \n\
             buildDefaultLayout()
         }
     }
-    
+
     // Syntax highlight the SQL Pane
     editor = CodeMirror.fromTextArea(document.getElementById("sql-textarea"), {
         lineNumbers: true,
@@ -298,7 +298,7 @@ ALTER TABLE tickets_detail \n\
                     return $.parseJSON(payload).data;
                 }
             }
-        },
+        }
     });
     tree = $('#tree').aciTree('api');
 
@@ -310,22 +310,20 @@ ALTER TABLE tickets_detail \n\
             var menu = { };
             var createMenu = { };
 
-            {% if create_items is defined %}
-            {% for create_item in create_items %}
-            if ($.inArray(tree.itemData(item)._type, {{ create_item.type }}) >= 0) {
+            {% for create_item in menu_items.create_items %}
+            if ($.inArray(tree.itemData(item)._type, {{ create_item.types | tojson }}) >= 0) {
                 createMenu['{{ create_item.name }}'] = { name: '{{ create_item.label }}', callback: function() { {{ create_item.function }}() }};
             }
-            {% endfor %}{% endif %}
-            
-            menu["create"] = { "name": "Create" }            
+            {% endfor %}
+
+            menu["create"] = { "name": "Create" }
             menu["create"]["items"] = createMenu
-            
-            {% if context_items is defined %}
-            {% for context_item in context_items %}
+
+            {% for context_item in menu_items.context_items %}
             if (tree.itemData(item)._type == '{{ context_item.type }}') {
                 menu['{{ context_item.name }}'] = { name: '{{ context_item.label }}', callback: function() { {{ context_item.onclick }} }};
             }
-            {% endfor %}{% endif %}
+            {% endfor %}
             return {
                 autoHide: true,
                 items: menu,
@@ -333,7 +331,7 @@ ALTER TABLE tickets_detail \n\
             };
         }
     });
-    
+
     // Treeview event handler
     $('#tree').on('acitree', function(event, api, item, eventName, options){
         switch (eventName){
@@ -343,9 +341,11 @@ ALTER TABLE tickets_detail \n\
         }
     });
 
-    
+
     // Setup the menus
     enable_disable_menus()
 });
 
-
+{% for snippet in jssnippets %}
+    {{ snippet }}
+{% endfor %}
diff --git a/web/pgadmin/browser/views.py b/web/pgadmin/browser/views.py
deleted file mode 100644
index 8ae6e80..0000000
--- a/web/pgadmin/browser/views.py
+++ /dev/null
@@ -1,214 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""A blueprint module implementing the core pgAdmin browser."""
-MODULE_NAME = 'browser'
-
-from flask import Blueprint, Response, current_app, render_template, url_for
-from flaskext.gravatar import Gravatar
-from flask.ext.security import login_required
-from flask.ext.login import current_user
-from inspect import getmoduleinfo, getmembers
-
-from . import sub_nodes
-from pgadmin.browser import all_nodes
-from pgadmin import modules
-from pgadmin.settings import get_setting
-from pgadmin.utils.ajax import make_json_response
-
-
-import config
-
-# Initialise the module
-blueprint = Blueprint(MODULE_NAME, __name__, static_folder='static', template_folder='templates', url_prefix='/' + MODULE_NAME)
-
-@blueprint.route("/")
-@login_required
-def index():
-    """Render and process the main browser window."""
-    # Get the Gravatar
-    gravatar = Gravatar(current_app,
-                        size=100,
-                        rating='g',
-                        default='retro',
-                        force_default=False,
-                        use_ssl=False,
-                        base_url=None)
-
-    # Get the plugin elements from the module
-    file_items = [ ]
-    edit_items = [ ]
-    tools_items = [ ]
-    management_items = [ ]
-    help_items = [ ]
-    stylesheets = [ ]
-    scripts = [ ]
-
-    modules_and_nodes = modules + all_nodes
-
-    # Add browser stylesheets
-    stylesheets.append(url_for('static', filename='css/codemirror/codemirror.css'))
-
-    if config.DEBUG:
-        stylesheets.append(url_for('static', filename='css/wcDocker/wcDockerSkeleton.css'))
-    else:
-        stylesheets.append(url_for('static', filename='css/wcDocker/wcDockerSkeleton.min.css'))
-
-    stylesheets.append(url_for('static', filename='css/wcDocker/theme.css'))
-    stylesheets.append(url_for('static', filename='css/jQuery-contextMenu/jquery.contextMenu.css'))
-    stylesheets.append(url_for('browser.static', filename='css/browser.css'))
-    stylesheets.append(url_for('browser.static', filename='css/aciTree/css/aciTree.css'))
-    stylesheets.append(url_for('browser.browser_css'))
-
-    # Add browser scripts
-    scripts.append(url_for('static', filename='js/codemirror/codemirror.js'))
-    scripts.append(url_for('static', filename='js/codemirror/mode/sql.js'))
-
-    if config.DEBUG:
-        scripts.append(url_for('static', filename='js/wcDocker/wcDocker.js'))
-    else:
-        scripts.append(url_for('static', filename='js/wcDocker/wcDocker.min.js'))
-
-    scripts.append(url_for('static', filename='js/jQuery-contextMenu/jquery.ui.position.js'))
-    scripts.append(url_for('static', filename='js/jQuery-contextMenu/jquery.contextMenu.js'))
-    scripts.append(url_for('browser.static', filename='js/aciTree/jquery.aciPlugin.min.js'))
-    scripts.append(url_for('browser.static', filename='js/aciTree/jquery.aciTree.dom.js'))
-    scripts.append(url_for('browser.static', filename='js/aciTree/jquery.aciTree.min.js'))
-    scripts.append(url_for('browser.browser_js'))
-
-    for module in modules_and_nodes:
-        # Get the edit menu items
-        if 'hooks' in dir(module) and 'get_file_menu_items' in dir(module.hooks):
-            file_items.extend(module.hooks.get_file_menu_items())
-
-        # Get the edit menu items
-        if 'hooks' in dir(module) and 'get_edit_menu_items' in dir(module.hooks):
-            edit_items.extend(module.hooks.get_edit_menu_items())
-
-        # Get the tools menu items
-        if 'hooks' in dir(module) and 'get_tools_menu_items' in dir(module.hooks):
-            tools_items.extend(module.hooks.get_tools_menu_items())
-
-        # Get the management menu items
-        if 'hooks' in dir(module) and 'get_management_menu_items' in dir(module.hooks):
-            management_items.extend(module.hooks.get_management_menu_items())
-
-        # Get the help menu items
-        if 'hooks' in dir(module) and 'get_help_menu_items' in dir(module.hooks):
-            help_items.extend(module.hooks.get_help_menu_items())
-
-        # Get any stylesheets
-        if 'hooks' in dir(module) and 'get_stylesheets' in dir(module.hooks):
-            stylesheets += module.hooks.get_stylesheets()
-
-        # Get any scripts
-        if 'hooks' in dir(module) and 'get_scripts' in dir(module.hooks):
-            scripts += module.hooks.get_scripts()
-
-    file_items = sorted(file_items, key=lambda k: k['priority'])
-    edit_items = sorted(edit_items, key=lambda k: k['priority'])
-    tools_items = sorted(tools_items, key=lambda k: k['priority'])
-    management_items = sorted(management_items, key=lambda k: k['priority'])
-    help_items = sorted(help_items, key=lambda k: k['priority'])
-
-    return render_template(MODULE_NAME + '/index.html',
-                           username=current_user.email,
-                           file_items=file_items,
-                           edit_items=edit_items,
-                           tools_items=tools_items,
-                           management_items=management_items,
-                           help_items=help_items,
-                           stylesheets = stylesheets,
-                           scripts = scripts)
-
-@blueprint.route("/browser.js")
-@login_required
-def browser_js():
-    """Render and return JS snippets from the nodes and modules."""
-    snippets = ''
-    modules_and_nodes = modules + all_nodes
-
-    # Load the core browser code first
-
-    # Get the context menu items
-    standard_items = [ ]
-    create_items = [ ]
-    context_items = [ ]
-    panel_items = [ ]
-
-    for module in modules_and_nodes:
-        # Get any standard menu items
-        if 'hooks' in dir(module) and 'get_standard_menu_items' in dir(module.hooks):
-            standard_items.extend(module.hooks.get_standard_menu_items())
-
-        # Get any create menu items
-        if 'hooks' in dir(module) and 'get_create_menu_items' in dir(module.hooks):
-            create_items.extend(module.hooks.get_create_menu_items())
-
-        # Get any context menu items
-        if 'hooks' in dir(module) and 'get_context_menu_items' in dir(module.hooks):
-            context_items.extend(module.hooks.get_context_menu_items())
-
-        # Get any panels
-        if 'hooks' in dir(module) and 'get_panels' in dir(module.hooks):
-            panel_items += module.hooks.get_panels()
-
-    standard_items = sorted(standard_items, key=lambda k: k['priority'])
-    create_items = sorted(create_items, key=lambda k: k['priority'])
-    context_items = sorted(context_items, key=lambda k: k['priority'])
-    panel_items = sorted(panel_items, key=lambda k: k['priority'])
-
-    layout = get_setting('Browser/Layout', default='')
-
-    snippets += render_template('browser/js/browser.js',
-                                layout = layout,
-                                standard_items = standard_items,
-                                create_items = create_items,
-                                context_items = context_items,
-                                panel_items = panel_items)
-
-    # Add module and node specific code
-    for module in modules_and_nodes:
-        if 'hooks' in dir(module) and 'get_script_snippets' in dir(module.hooks):
-            snippets += module.hooks.get_script_snippets()
-
-    resp = Response(response=snippets,
-                status=200,
-                mimetype="application/javascript")
-
-    return resp
-
-@blueprint.route("/browser.css")
-@login_required
-def browser_css():
-    """Render and return CSS snippets from the nodes and modules."""
-    snippets = ''
-    modules_and_nodes = modules + all_nodes
-
-    for module in modules_and_nodes:
-        if 'hooks' in dir(module) and 'get_css_snippets' in dir(module.hooks):
-            snippets += module.hooks.get_css_snippets()
-
-    resp = Response(response=snippets,
-                status=200,
-                mimetype="text/css")
-
-    return resp
-
-
-@blueprint.route("/nodes/")
-@login_required
-def get_nodes():
-    """Build a list of treeview nodes from the child nodes."""
-    value = '['
-    nodes = []
-    for node in sub_nodes:
-        if hasattr(node, 'hooks') and hasattr(node.hooks, 'get_nodes'):
-            nodes.extend(node.hooks.get_nodes())
-    return make_json_response(data=nodes)
diff --git a/web/pgadmin/help/__init__.py b/web/pgadmin/help/__init__.py
index e69de29..6d753f8 100644
--- a/web/pgadmin/help/__init__.py
+++ b/web/pgadmin/help/__init__.py
@@ -0,0 +1,66 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2015, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""A blueprint module implementing the pgAdmin help system."""
+MODULE_NAME = 'help'
+
+from pgadmin.utils import PgAdminModule
+from pgadmin.utils.menu import MenuItem, Panel
+from flask.ext.babel import gettext
+from flask import url_for
+import config
+
+
+class HelpModule(PgAdminModule):
+
+    def get_own_menuitems(self):
+        """Return a (set) of dicts of help menu items, with name, priority, URL,
+        target and onclick code."""
+        return { 'help_items': [
+            MenuItem(name='mnu_online_help',
+                     label=gettext('Online Help'),
+                     priority=100,
+                     target='_new',
+                     url=url_for('help.static', filename='index.html')),
+
+            MenuItem(name='mnu_pgadmin_website',
+                     label= gettext('pgAdmin Website'),
+                     priority= 200,
+                     target= '_new',
+                     url= 'http://www.pgadmin.org/' ),
+
+            MenuItem(name= 'mnu_postgresql_website',
+                     label= gettext('PostgreSQL Website'),
+                     priority= 300,
+                     target= '_new',
+                     url= 'http://www.postgresql.org/' )]}
+
+    def get_panels(self):
+        return [
+            Panel(
+                name='pnl_online_help',
+                priority=100,
+                title=gettext('Online Help'),
+                content=url_for('help.static', filename='index.html')),
+
+            Panel(name='pnl_pgadmin_website',
+                  priority=200,
+                  title=gettext('pgAdmin Website'),
+                  content='http://www.pgadmin.org/'),
+
+            Panel(name='pnl_postgresql_website',
+                  priority=300,
+                  title=gettext('PostgreSQL Website'),
+                  content='http://www.postgresql.org/')]
+
+
+
+# Initialise the module
+blueprint = HelpModule(MODULE_NAME, __name__, static_url_path='/help',
+                       static_folder=config.HELP_PATH)
diff --git a/web/pgadmin/help/hooks.py b/web/pgadmin/help/hooks.py
deleted file mode 100644
index 9fb5bc6..0000000
--- a/web/pgadmin/help/hooks.py
+++ /dev/null
@@ -1,73 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""Browser integration functions for the Help module."""
-
-from flask import url_for
-from flask.ext.babel import gettext
-
-import config
-
-def get_help_menu_items():
-    """Return a (set) of dicts of help menu items, with name, priority, URL, 
-    target and onclick code."""
-    return [{'name': 'mnu_online_help',
-             'label': gettext('Online Help'), 
-             'priority': 100, 
-             'target': '_new',
-             'url': url_for('help.static', filename='index.html') },
-             
-            {'name': 'mnu_pgadmin_website',
-             'label': gettext('pgAdmin Website'), 
-             'priority': 200, 
-             'target': '_new',
-             'url': 'http://www.pgadmin.org/' },
-             
-             {'name': 'mnu_postgresql_website',
-             'label': gettext('PostgreSQL Website'), 
-             'priority': 300, 
-             'target': '_new',
-             'url': 'http://www.postgresql.org/' }]
-    
-def get_panels():
-    """Return a (set) of dicts describing panels to create in the browser. Fields
-    are name, priority, title, width, height, isIframe, showTitle, isCloseable, 
-    isPrivate and content"""
-    return [{'name': 'pnl_online_help',
-             'priority': 100,
-             'title': gettext('Online Help'),
-             'width': 500,
-             'height': 600,
-             'isIframe': True,
-             'showTitle': True,
-             'isCloseable': True,
-             'isPrivate': False,
-             'content': url_for('help.static', filename='index.html') },
-             
-            {'name': 'pnl_pgadmin_website',
-             'priority': 200,
-             'title': gettext('pgAdmin Website'),
-             'width': 500,
-             'height': 600,
-             'isIframe': True,
-             'showTitle': True,
-             'isCloseable': True,
-             'isPrivate': False,
-             'content': 'http://www.pgadmin.org/' },
-             
-            {'name': 'pnl_postgresql_website',
-             'priority': 300,
-             'title': gettext('PostgreSQL Website'),
-             'width': 500,
-             'height': 600,
-             'isIframe': True,
-             'showTitle': True,
-             'isCloseable': True,
-             'isPrivate': False,
-             'content': 'http://www.postgresql.org/' }]
\ No newline at end of file
diff --git a/web/pgadmin/help/views.py b/web/pgadmin/help/views.py
deleted file mode 100644
index 10ad910..0000000
--- a/web/pgadmin/help/views.py
+++ /dev/null
@@ -1,18 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""A blueprint module implementing the pgAdmin help system."""
-MODULE_NAME = 'help'
-
-from flask import Blueprint
-
-import config
-
-# Initialise the module
-blueprint = Blueprint(MODULE_NAME, __name__, static_url_path='/help', static_folder=config.HELP_PATH)
diff --git a/web/pgadmin/misc/__init__.py b/web/pgadmin/misc/__init__.py
index e69de29..1f6973c 100644
--- a/web/pgadmin/misc/__init__.py
+++ b/web/pgadmin/misc/__init__.py
@@ -0,0 +1,25 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2015, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""A blueprint module providing utility functions for the application."""
+MODULE_NAME = 'misc'
+
+from pgadmin.utils import PgAdminModule
+
+# Initialise the module
+blueprint = PgAdminModule(MODULE_NAME, __name__,
+                          url_prefix='')
+
+##########################################################################
+# A special URL used to "ping" the server
+##########################################################################
+@blueprint.route("/ping")
+def ping():
+    """Generate a "PING" response to indicate that the server is alive."""
+    return "PING"
diff --git a/web/pgadmin/misc/views.py b/web/pgadmin/misc/views.py
deleted file mode 100644
index 9b166b2..0000000
--- a/web/pgadmin/misc/views.py
+++ /dev/null
@@ -1,27 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""A blueprint module providing utility functions for the application."""
-MODULE_NAME = 'misc'
-
-import config
-from flask import Blueprint, render_template
-from flask.ext.security import login_required
-
-# Initialise the module
-blueprint = Blueprint(MODULE_NAME, __name__, static_folder='static', template_folder='templates', url_prefix='')
-
-##########################################################################
-# A special URL used to "ping" the server
-##########################################################################
-@blueprint.route("/ping")
-def ping():
-    """Generate a "PING" response to indicate that the server is alive."""
-    return "PING"
-
diff --git a/web/pgadmin/redirects/__init__.py b/web/pgadmin/redirects/__init__.py
index e69de29..31c6fe5 100644
--- a/web/pgadmin/redirects/__init__.py
+++ b/web/pgadmin/redirects/__init__.py
@@ -0,0 +1,19 @@
+from pgadmin import PgAdminModule
+from flask.ext.security import login_required
+from flask import redirect, url_for
+
+MODULE_NAME = 'redirects'
+
+blueprint = PgAdminModule(MODULE_NAME, __name__,
+                          url_prefix='/')
+
+@blueprint.route('/')
+@login_required
+def index():
+    """Redirect users hitting the root to the browser"""
+    return redirect(url_for('browser.index'))
+
+@blueprint.route('/favicon.ico')
+def favicon():
+    """Redirect to the favicon"""
+    return redirect(url_for('static', filename='favicon.ico'))
diff --git a/web/pgadmin/redirects/views.py b/web/pgadmin/redirects/views.py
deleted file mode 100644
index ee323c9..0000000
--- a/web/pgadmin/redirects/views.py
+++ /dev/null
@@ -1,29 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""A blueprint module providing URL redirects."""
-MODULE_NAME = 'redirects'
-
-import config
-from flask import Blueprint, redirect, url_for
-from flask.ext.security import login_required
-
-# Initialise the module
-blueprint = Blueprint(MODULE_NAME, __name__)
-
-@blueprint.route('/')
-@login_required
-def index():
-    """Redirect users hitting the root to the browser"""
-    return redirect(url_for('browser.index'))
-
-@blueprint.route('/favicon.ico')
-def favicon():
-    """Redirect to the favicon"""
-    return redirect(url_for('static', filename='favicon.ico'))
diff --git a/web/pgadmin/settings/__init__.py b/web/pgadmin/settings/__init__.py
index fb9f1a6..e96f50e 100644
--- a/web/pgadmin/settings/__init__.py
+++ b/web/pgadmin/settings/__init__.py
@@ -14,20 +14,95 @@ from flask.ext.login import current_user
 from flask.ext.sqlalchemy import SQLAlchemy
 
 from settings_model import db, Setting
+import traceback
+from flask import Blueprint, Response, abort, request, render_template
+from flask.ext.security import login_required
+
+import config
+from pgadmin.utils.ajax import make_json_response
+from pgadmin.utils import PgAdminModule
+
+MODULE_NAME = 'settings'
 
 def store_setting(setting, value):
     """Set a configuration setting for the current user."""
     data = Setting(user_id=current_user.id, setting=setting, value=value)
-    
+
     db.session.merge(data)
     db.session.commit()
-    
+
 def get_setting(setting, default=None):
-    """Retrieve a configuration setting for the current user, or return the 
+    """Retrieve a configuration setting for the current user, or return the
     default value specified by the caller."""
     data = Setting.query.filter_by(user_id=current_user.id, setting=setting).first()
-    
+
     if not data or data.value is None:
         return default
     else:
-        return data.value
\ No newline at end of file
+        return data.value
+
+# Initialise the module
+blueprint = PgAdminModule(MODULE_NAME, __name__, template_folder='templates', url_prefix='/' + MODULE_NAME)
+
+@blueprint.route("/settings.js")
+@login_required
+def script():
+    """Render the required Javascript"""
+    return Response(response=render_template("settings/settings.js"),
+                    status=200,
+                    mimetype="application/javascript")
+
+@blueprint.route("/store", methods=['POST'])
+@blueprint.route("/store/<setting>/<value>", methods=['GET'])
+@login_required
+def store(setting=None, value=None):
+    """Store a configuration setting, or if this is a POST request and a
+    count value is present, store multiple settings at once."""
+    success = 1
+    errorcode = 0
+    errormsg = ''
+
+    try:
+        if request.method == 'POST':
+            if 'count' in request.form:
+                for x in range(int(request.form['count'])):
+                    store_setting(request.form['setting%d' % (x+1)], request.form['value%d' % (x+1)])
+            else:
+                store_setting(request.form['setting'], request.form['value'])
+        else:
+            store_setting(setting, value)
+    except Exception as e:
+        success = 0
+        errormsg = e.message
+
+    return make_json_response(success=success,
+                              errormsg=errormsg,
+                              info=traceback.format_exc(),
+                              result=request.form)
+
+@blueprint.route("/get", methods=['POST'])
+@blueprint.route("/get/<setting>", methods=['GET'])
+@blueprint.route("/get/<setting>/<default>", methods=['GET'])
+@login_required
+def get(setting=None, default=None):
+    """Get a configuration setting."""
+    if request.method == 'POST':
+        setting = request.form['setting']
+        default = request.form['default']
+
+    success = 1
+    errorcode = 0
+    errormsg = ''
+
+    try:
+        value = get_setting(setting, default)
+    except Exception as e:
+        success = 0
+        errormsg = e.message
+
+    return make_json_response(success=success,
+                              errormsg=errormsg,
+                              info=traceback.format_exc(),
+                              result=request.form)
+
+
diff --git a/web/pgadmin/settings/views.py b/web/pgadmin/settings/views.py
deleted file mode 100644
index 67e0d5a..0000000
--- a/web/pgadmin/settings/views.py
+++ /dev/null
@@ -1,83 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""Views for setting and storing configuration options."""
-MODULE_NAME = 'settings'
-
-import traceback
-from flask import Blueprint, Response, abort, request, render_template
-from flask.ext.security import login_required
-
-import config
-from pgadmin.utils.ajax import make_json_response
-from . import get_setting, store_setting
-
-# Initialise the module
-blueprint = Blueprint(MODULE_NAME, __name__, template_folder='templates', url_prefix='/' + MODULE_NAME)
-
-@blueprint.route("/settings.js")
-@login_required
-def script():
-    """Render the required Javascript"""
-    return Response(response=render_template("settings/settings.js"),
-                    status=200,
-                    mimetype="application/javascript")
-    
-@blueprint.route("/store", methods=['POST'])
-@blueprint.route("/store/<setting>/<value>", methods=['GET'])
-@login_required
-def store(setting=None, value=None):
-    """Store a configuration setting, or if this is a POST request and a  
-    count value is present, store multiple settings at once."""
-    success = 1
-    errorcode = 0
-    errormsg = ''
-    
-    try:
-        if request.method == 'POST':
-            if 'count' in request.form:
-                for x in range(int(request.form['count'])):
-                    store_setting(request.form['setting%d' % (x+1)], request.form['value%d' % (x+1)])
-            else:
-                store_setting(request.form['setting'], request.form['value'])
-        else:
-            store_setting(setting, value)
-    except Exception as e:
-        success = 0
-        errormsg = e.message
-        
-    return make_json_response(success=success, 
-                              errormsg=errormsg, 
-                              info=traceback.format_exc(), 
-                              result=request.form)
-
-@blueprint.route("/get", methods=['POST'])
-@blueprint.route("/get/<setting>", methods=['GET'])
-@blueprint.route("/get/<setting>/<default>", methods=['GET'])
-@login_required
-def get(setting=None, default=None):
-    """Get a configuration setting."""
-    if request.method == 'POST':
-        setting = request.form['setting']
-        default = request.form['default']
-    
-    success = 1
-    errorcode = 0
-    errormsg = ''
-    
-    try:
-        value = get_setting(setting, default)
-    except Exception as e:
-        success = 0
-        errormsg = e.message
-
-    return make_json_response(success=success, 
-                              errormsg=errormsg, 
-                              info=traceback.format_exc(), 
-                              result=request.form)
diff --git a/web/pgadmin/templates/base.html b/web/pgadmin/templates/base.html
index 19cc511..5a36182 100755
--- a/web/pgadmin/templates/base.html
+++ b/web/pgadmin/templates/base.html
@@ -20,23 +20,21 @@
         {% if config.DEBUG %}<link rel="stylesheet" href="{{ url_for('static', filename='css/alertifyjs/themes/bootstrap.css') }}" />{% else %}<link rel="stylesheet" href="{{ url_for('static', filename='css/alertifyjs/themes/bootstrap.min.css') }}" />{% endif %}
         {% if config.DEBUG %}<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-theme.min.css') }}">{% else %}<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-theme.min.css') }}">{% endif %}
         <link rel="stylesheet" href="{{ url_for('static', filename='css/overrides.css') }}">
-        {% if stylesheets is defined %}
         <!-- View specified stylesheets -->
-        {% for stylesheet in stylesheets %}
-        <link rel="stylesheet" href="{{ stylesheet }}">{% endfor %}
-        {% endif %}
-        
+        {% for stylesheet in current_app.stylesheets %}
+        <link rel="stylesheet" href="{{ stylesheet }}">
+        {% endfor %}
         <!-- Base template scripts -->
         <script src="{{ url_for('static', filename='js/modernizr-2.6.2-respond-1.1.0.min.js') }}"></script>
         {% if config.DEBUG %}<script src="{{ url_for('static', filename='js/jquery-1.11.2.js') }}">{% else %}<script src="{{ url_for('static', filename='js/jquery-1.11.2.min.js') }}">{% endif %}</script>
         {% if config.DEBUG %}<script src="{{ url_for('static', filename='js/bootstrap.js') }}">{% else %}<script src="{{ url_for('static', filename='js/bootstrap.min.js') }}">{% endif %}</script>
         {% if config.DEBUG %}<script src="{{ url_for('static', filename='js/alertifyjs/alertify.js') }}">{% else %}<script src="{{ url_for('static', filename='js/alertifyjs/alertify.min.js') }}">{% endif %}</script>
         <script src="{{ url_for('static', filename='js/alertifyjs/pgadmin.defaults.js') }}"></script>
-        {% if scripts is defined %}
         <!-- View specified scripts -->
-        {% for script in scripts %}
-        <script src="{{ script }}"></script>{% endfor %}
-        {% endif %}
+
+        {% for script in current_app.javascripts %}
+        <script src="{{ script }}"></script>
+        {% endfor %}
     </head>
     <body>
         <!--[if lt IE 7]>
diff --git a/web/pgadmin/test/__init__.py b/web/pgadmin/test/__init__.py
index e69de29..6ca55ef 100644
--- a/web/pgadmin/test/__init__.py
+++ b/web/pgadmin/test/__init__.py
@@ -0,0 +1,70 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2015, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""Browser integration functions for the Test module."""
+MODULE_NAME = 'test'
+from flask.ext.security import login_required
+from flask import render_template, url_for
+from flask.ext.babel import gettext
+from pgadmin.utils import PgAdminModule
+from pgadmin.utils.menu import MenuItem
+from time import time, ctime
+
+class TestModule(PgAdminModule):
+
+    def get_own_menuitems(self):
+        return {'file_items': [
+            MenuItem(name='mnu_generate_test_html',
+                     label=gettext('Generated Test HTML'),
+                     priority=100,
+                     url=url_for('test.generated')),
+            MenuItem(name='mnu_test_alert',
+                     label=gettext('Test Alert'),
+                     priority=200,
+                     url='#',
+                     onclick='test_alert()'),
+            MenuItem(name='mnu_test_confirm',
+                     label=gettext('Test Confirm'),
+                     priority=300,
+                     url='#',
+                     onclick='test_confirm()'),
+            MenuItem(name='mnu_test_dialog',
+                     label=gettext('Test Dialog'),
+                     priority=400,
+                     url='#',
+                     onclick='test_dialog()'),
+            MenuItem(name='mnu_test_prompt',
+                     label=gettext('Test Prompt'),
+                     priority=500,
+                     url='#',
+                     onclick='test_prompt()'),
+            MenuItem(name='mnu_test_notifier',
+                     label=gettext('Test Notifier'),
+                     priority=600,
+                     url='#',
+                     onclick='test_notifier()')
+        ]}
+
+    def get_own_javascripts(self):
+        return [ url_for('test.static', filename='js/test.js') ]
+
+# Initialise the module
+blueprint = TestModule(MODULE_NAME, __name__)
+
+@blueprint.route("/generated")
+@login_required
+def generated():
+    """Generate a simple test page to demonstrate that output can be rendered."""
+    output = """
+Today is <b>%s</b>
+<br />
+<i>This is Flask-generated HTML.</i>
+<br /><br />
+<a href="http://www.pgadmin.org/">%s v%s</a>""" % (ctime(time()), config.APP_NAME, config.APP_VERSION)
+    return output
diff --git a/web/pgadmin/test/hooks.py b/web/pgadmin/test/hooks.py
deleted file mode 100644
index ba8f9a8..0000000
--- a/web/pgadmin/test/hooks.py
+++ /dev/null
@@ -1,29 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""Browser integration functions for the Test module."""
-
-from flask import render_template, url_for
-from flask.ext.babel import gettext
-
-def get_file_menu_items():
-    """Return a (set) of dicts of file menu items, with name, priority, URL, 
-    target and onclick code."""
-    return [
-            {'name': 'mnu_generate_test_html', 'label': gettext('Generated Test HTML'), 'priority': 100, 'url': url_for('test.generated')},
-            {'name': 'mnu_test_alert', 'label': gettext('Test Alert'), 'priority': 200, 'url': '#', 'onclick': 'test_alert()'},
-            {'name': 'mnu_test_confirm', 'label': gettext('Test Confirm'), 'priority': 300, 'url': '#', 'onclick': 'test_confirm()'},
-            {'name': 'mnu_test_dialog', 'label': gettext('Test Dialog'), 'priority': 400, 'url': '#', 'onclick': 'test_dialog()'},
-            {'name': 'mnu_test_prompt', 'label': gettext('Test Prompt'), 'priority': 500, 'url': '#', 'onclick': 'test_prompt()'},
-            {'name': 'mnu_test_notifier', 'label': gettext('Test Notifier'), 'priority': 600, 'url': '#', 'onclick': 'test_notifier()'},
-           ]
-    
-def get_scripts():
-    """Return a list of script URLs to include in the rendered page header"""
-    return [ url_for('test.static', filename='js/test.js') ]
\ No newline at end of file
diff --git a/web/pgadmin/test/views.py b/web/pgadmin/test/views.py
deleted file mode 100644
index 01088d5..0000000
--- a/web/pgadmin/test/views.py
+++ /dev/null
@@ -1,35 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2015, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-"""A blueprint module providing utility functions for the application."""
-MODULE_NAME = 'test'
-
-import config
-from flask import Blueprint, render_template
-from flask.ext.security import login_required
-from time import time, ctime
-
-# Initialise the module
-blueprint = Blueprint(MODULE_NAME, __name__, static_folder='static', template_folder='templates', url_prefix='/' + MODULE_NAME)
-
-##########################################################################
-# A test page
-##########################################################################
-@blueprint.route("/generated")
-@login_required
-def generated():
-    """Generate a simple test page to demonstrate that output can be rendered."""
-    output = """
-Today is <b>%s</b>
-<br />
-<i>This is Flask-generated HTML.</i>
-<br /><br />
-<a href="http://www.pgadmin.org/">%s v%s</a>""" % (ctime(time()), config.APP_NAME, config.APP_VERSION)
-
-    return output
diff --git a/web/pgadmin/utils/__init__.py b/web/pgadmin/utils/__init__.py
index e69de29..9370136 100644
--- a/web/pgadmin/utils/__init__.py
+++ b/web/pgadmin/utils/__init__.py
@@ -0,0 +1,87 @@
+from flask import Blueprint
+from collections import defaultdict
+from operator import attrgetter
+import sys
+
+
+
+class PgAdminModule(Blueprint):
+    """
+    Base class for every PgAdmin Module.
+
+    This class defines a set of method and attributes that
+    every module should implement.
+    """
+
+    def __init__(self, name, import_name, **kwargs):
+        kwargs.setdefault('url_prefix', '/' + name)
+        kwargs.setdefault('template_folder', 'templates')
+        kwargs.setdefault('static_folder', 'static')
+        self.submodules = []
+        super(PgAdminModule, self).__init__(name, import_name, **kwargs)
+
+    def register(self, app, options, first_registration=False):
+        """
+        Override the default register function to automagically register
+        sub-modules at once.
+        """
+        if first_registration:
+            self.submodules = list(app.find_submodules(self.import_name))
+        super(PgAdminModule, self).register(app, options, first_registration)
+        for module in self.submodules:
+            app.register_blueprint(module)
+
+    def get_own_stylesheets(self):
+        """
+        Returns:
+            list: the stylesheets used by this module, not including any
+                stylesheet needed by the submodules.
+        """
+        return []
+
+    def get_own_javascripts(self):
+        """
+        Returns:
+            list: the javascripts used by this module, not including
+                any script needed by the submodules.
+        """
+        return []
+
+    def get_own_menuitems(self):
+        """
+        Returns:
+            dict: the menuitems for this module, not including
+                any needed from the submodules.
+        """
+        return defaultdict(list)
+
+    def get_panels(self):
+        """
+        Returns:
+            list: a list of panel objects to add
+        """
+        return []
+
+    @property
+    def stylesheets(self):
+        stylesheets = self.get_own_stylesheets()
+        for module in self.submodules:
+            stylesheets.extend(module.stylesheets)
+        return stylesheets
+
+    @property
+    def javascripts(self):
+        javascripts = self.get_own_javascripts()
+        for module in self.submodules:
+            javascripts.extend(module.javascripts)
+        return javascripts
+
+    @property
+    def menu_items(self):
+        menu_items = self.get_own_menuitems()
+        for module in self.submodules:
+            for key, value in module.menu_items.items():
+                menu_items[key].extend(value)
+        menu_items = {key: sorted(values, key=attrgetter('priority'))
+                      for key, values in menu_items.items()}
+        return menu_items
diff --git a/web/pgadmin/utils/menu.py b/web/pgadmin/utils/menu.py
new file mode 100644
index 0000000..e8628ba
--- /dev/null
+++ b/web/pgadmin/utils/menu.py
@@ -0,0 +1,27 @@
+from collections import namedtuple
+
+PRIORITY = 100
+
+class MenuItem(object):
+
+    def __init__(self, **kwargs):
+        self.__dict__.update(**kwargs)
+
+class Panel(object):
+
+    def __init__(self, name, title, content, width=500, height=600, isIframe=True,
+                 showTitle=True, isCloseable=True, isPrivate=False, priority=None):
+        self.name = name
+        self.title = title
+        self.content = content
+        self.width = width
+        self.height = height
+        self.isIfframe = isIframe
+        self.showTitle = showTitle
+        self.isCloseable = isCloseable
+        self.isPrivate = isPrivate
+        if priority is None:
+            global PRIORITY
+            PRIORITY += 100
+            priority = PRIORITY
+        self.priority = priority
